package doh.logic;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
import doh.util.*;
import doh.entity.*;
import doh.login.LoginService;
import javax.persistence.*;

/*
 * The GameInstance class encapsulates a RPS session between two players
 * It maintains the state of the match, the rounds won, throws of each player,
 * and other bookeeping information.
 */
public class GameInstance implements java.io.Serializable {

    private String p1;      // name of player1
    private String p2;      // name of player2
    private int wins1;      // number of rounds won by player1
    private int wins2;      // number of rounds won by player2
    private int bestOfN;    // number of rounds for the match, e.g. best of 3
    private int throw1; // player1's most recent throw
    private int throw2; // playe2's most recent throw
    private int roundWinner;            // winner of most recent round
    private boolean roundWon = false;   // flag to indicate end of a round
    private boolean p1go;               // flag to indicate that player1 has made her throw
    private boolean p2go;               // flag to indicate that player2 has made his throw
    private String throwStr;            // console message indicating throws of the current round
    private String winStr;              // console message indcating round conclusion
    private Properties p;               // used to pass round details to the participating players
    private GameSessionDetails gsd;     // bookeeping class to track statistics for update
    private EntityManager em;           // entity manager for object persistence
    private long lastThrow;             // keeps track of timeout for games
    private long timeout;               // timeout value from config file
    private boolean timeoutReached;     // determines if the timeout has been reached
    private static String[] throwsArray = {"", "Rock", "Paper", "Scissors"}; // names of possible throws
    private boolean gameWon;

    /*
     * Constructor, initializes a GameInstance
     */
    public GameInstance(String p1, String p2, EntityManager em) {
        this.em = em;
        this.p1 = p1;
        this.p2 = p2;
        this.gsd = new GameSessionDetails(new GamePlayer(p1, 0, 0, 0), new GamePlayer(p2, 0, 0, 0), false);
        SystemProperties dohProp = new SystemProperties();
        this.bestOfN = (dohProp.get_num_rounds() / 2) + 1;
        this.timeout = dohProp.get_timeout();
        timeoutReached = false;
        gameWon = false;
    }

    /**
     * Called when player makes a throw, updates bookeeping in GameSessionDetails
     * @param gsd, the object that maintains stats of the match
     * @param player, the userID of the player making the throw
     * @param t, the throw (rock, paper, or scissors) made
     */
    public void addThrow(GameSessionDetails gsd, String player, int t) {
        System.err.println("adding throw:" + player + "=" + t);
        if (player.equals(gsd.getPlayer1().getName())) {
            switch (t) {
                case 1:
                    gsd.getPlayer1().incRock();
                case 2:
                    gsd.getPlayer1().incPaper();
                case 3:
                    gsd.getPlayer1().incScissor();
            }
        } else if (player.equals(gsd.getPlayer2().getName())) {
            switch (t) {
                case 1:
                    gsd.getPlayer2().incRock();
                case 2:
                    gsd.getPlayer2().incPaper();
                case 3:
                    gsd.getPlayer2().incScissor();
            }
        }
    }

    /**
     * Queries database for ELO rating of a player
     * @param id, the userID of the player
     * @return int, the ELO rating of the player
     */
    public int getRating(String id) {
        Player myPlayer = em.find(Player.class, id);
        return myPlayer.get_rating();
    }

    /**
     * Helper method that adjusts stats for each participating player entity
     * @param p1, userID of player1
     * @param p2, userID of player2
     * @param winner, userID of the winner
     * @param adjustment, value of the ELO rating adjustment to be made
     * @param gsd, GameSessionDetails object that stores the stats of the match
     */
    public void adjust_stats(String p1, String p2, boolean winner, int adjustment, GameSessionDetails gsd) {
        Player player1 = em.find(Player.class, p1);
        Player player2 = em.find(Player.class, p2);
        if (winner) {
            player1.set_wins(player1.get_wins() + 1);
            player1.set_rating(player1.get_rating() + adjustment);
            player2.set_rating(player2.get_rating() - adjustment);
        } else {
            player1.set_rating(player1.get_rating() - adjustment);
            player2.set_rating(player2.get_rating() + adjustment);
            player2.set_wins(player2.get_wins() + 1);
        }
        player1.set_games(player1.get_games() + 1);
        System.err.println("p1 rocks old|new = " + player1.get_rocks() + "|" + gsd.getPlayer1().rock);
        player1.set_rocks(player1.get_rocks() + gsd.getPlayer1().rock);
        player1.set_papers(player1.get_papers() + gsd.getPlayer1().paper);
        player1.set_scissors(player1.get_scissors() + gsd.getPlayer1().scissor);
        player2.set_games(player2.get_games() + 1);
        player2.set_rocks(player2.get_rocks() + gsd.getPlayer2().rock);
        player2.set_papers(player2.get_papers() + gsd.getPlayer2().paper);
        player2.set_scissors(player2.get_scissors() + gsd.getPlayer2().scissor);
        player1.set_wratio((double) player1.get_wins() / player1.get_games());
        player2.set_wratio((double) player2.get_wins() / player2.get_games());
        player1.set_oratio((((player1.get_games() - 1) * player1.get_oratio()) + player2.get_wratio()) / player1.get_games());
        player2.set_oratio((((player2.get_games() - 1) * player2.get_oratio()) + player1.get_wratio()) / player2.get_games());
    }

    /**
     * Helper method that calculates ELO adjustment,
     * using a slightly modified version of the FIDE formula
     * @param r1, rating of player1
     * @param r2, rating of player2
     * @param winner, userID of the winner
     * @return int, the ELO rating adjustment value
     */
    public int calc_ELO_adjustment(int r1, int r2, boolean winner) throws FileNotFoundException, IOException {
        SystemProperties dohProp = new SystemProperties();
        int max_adj = dohProp.get_ELO_max_adj();
        double multiplier = 1.0;
        if (winner) {
            multiplier = (1 / (1 + Math.pow(10.0, (r1 - r2) / 400.0)));
        } else {
            multiplier = (1 / (1 + Math.pow(10.0, (r2 - r1) / 400.0)));
        }
        int adjustment = (int) Math.round(max_adj * multiplier);
        return adjustment;
    }

    /**
     * Adds a concluded game to the database, persisting stats
     * Making necessary adjustments to the Players and Games tables
     * @param gsd, the GameSessionDetails object storing the stats of the match
     */
    public void addGame(GameSessionDetails gsd) throws FileNotFoundException, IOException {
        String p1 = gsd.getPlayer1().name;
        String p2 = gsd.getPlayer2().name;
        int r1 = getRating(p1);
        int r2 = getRating(p2);
        boolean winner = gsd.getWinner();
        int adjustment = calc_ELO_adjustment(r1, r2, winner);
        adjust_stats(p1, p2, winner, adjustment, gsd);
        Game completedGame = new Game(p1, p2, r1, r2, winner, adjustment);
        em.persist(completedGame);
    }

    /**
     * Allows for each player to play the game - each method call
     * simulates one throw in one round.
     * @param player The player that is calling the method.
     * @param t The throw, 1 for rock, 2 for paper, 3 for scissors.
     * @return A Properties object containing information for the clients
     * on each round after it is completed.
     */
    public Properties throwObject(String player, int t) throws FileNotFoundException, IOException {
        // set initial properties
        roundWon = false;
        p = new Properties();
        p.setProperty("p1", p1);
        p.setProperty("p2", p2);

        // lets us know which user has entered this method
        if (player.equals(p1)) {
            addThrow(gsd, p1, t);
            throw1 = t;
            p1go = true;
        } else {
            addThrow(gsd, p2, t);
            throw2 = t;
            p2go = true;
        }

        // only the second user to start will enter this
        if (p1go && p2go) {
            // check if timed out or other user logged out
            if (timeoutReached
                    || (player.equals(p1) && !LoginService.isUserOnline(p2))
                    || (player.equals(p2) && !LoginService.isUserOnline(p1))) {
                if((player.equals(p1) && !LoginService.isUserOnline(p2))
                    || (player.equals(p2) && !LoginService.isUserOnline(p1))){
                    winStr = "Other player logged out!";
                } else {
                    winStr = "A player timed out!";
                }
                if(!gameWon){
                    if(timeoutReached){
                        if(player.equals(p1)){
                            wins1 = 0;
                            wins2 = bestOfN;
                        } else {
                            wins1 = bestOfN;
                            wins2 = 0;
                        }
                    } else {
                        if(player.equals(p2)){
                            wins1 = 0;
                            wins2 = bestOfN;
                        } else {
                            wins1 = bestOfN;
                            wins2 = 0;
                        }
                    }
                }
                throwStr = "";
                p.setProperty("status", winStr);
                p.setProperty("throws", throwStr);
                if (wins1 == bestOfN) {
                    if (!gameWon) {
                        gsd.changeWinner(true);
                        addGame(gsd);
                        gameWon = true;
                    }
                    p.setProperty("end", "1");
                } else if (wins2 == bestOfN) {
                    if (!gameWon) {
                        gsd.changeWinner(false);
                        addGame(gsd);
                        gameWon = true;
                    }
                    p.setProperty("end", "2");
                }
                gameWon = true;
                return p;
            }

            // determine winner
            roundWinner = getWinner();
            winStr = getStatusMessage(roundWinner);
            throwStr = getThrowMessage();

            if (wins1 == bestOfN) {
                gsd.changeWinner(true);
                addGame(gsd);
                p.setProperty("end", "1");
                gameWon = true;
            } else if (wins2 == bestOfN) {
                gsd.changeWinner(false);
                addGame(gsd);
                p.setProperty("end", "2");
                gameWon = true;
            } else {
                p.setProperty("end", "false");
            }

            p.setProperty("status", winStr);
            p.setProperty("throws", throwStr);

            roundWon = true;
        } else {
            lastThrow = System.currentTimeMillis();
            // set min priority so we don't clog the system
            Thread.currentThread().setPriority(Thread.MIN_PRIORITY+1);
        }

        // first user will get stuck here, until the second
        // player goes
        // second user will pass straight through
        while (!roundWon) {
            if (System.currentTimeMillis() > lastThrow + timeout
                    || (player.equals(p1) && !LoginService.isUserOnline(p2))
                    || (player.equals(p2) && !LoginService.isUserOnline(p1))) {
                timeoutReached = true;
                //win by default - timeout reached
                p.setProperty("throws", "");
                if((player.equals(p1) && !LoginService.isUserOnline(p2))
                    || (player.equals(p2) && !LoginService.isUserOnline(p1))){
                    p.setProperty("status", "Other player logged out!");
                } else {
                   p.setProperty("status", "A player timed out!");
                }
                if (player.equals(p1)) {
                    p.setProperty("end", "1");
                    wins1 = bestOfN;
                    wins2 = 0;
                    gsd.changeWinner(true);
                    addGame(gsd);
                } else {
                    p.setProperty("end", "2");
                    wins1 = 0;
                    wins2 = bestOfN;
                    gsd.changeWinner(false);
                    addGame(gsd);
                }
                gameWon = true;
                return p;
            }
        }

        p1go = false;
        p2go = false;
        Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
        return p;
    }

    /**
     * Get the winner depending on the two throws
     * 1 - rock
     * 2 - paper
     * 3 - scissors
     * @return int for winner, 1 for p1, 2 for p2
     */
    private int getWinner() {
        if (throw1 == 1) {
            if (throw2 == 1) {
                return 0;
            }
            if (throw2 == 2) {
                return 2;
            }
            return 1;
        }
        if (throw1 == 2) {
            if (throw2 == 1) {
                return 1;
            }
            if (throw2 == 2) {
                return 0;
            }
            return 2;
        }
        if (throw1 == 3) {
            if (throw2 == 1) {
                return 2;
            }
            if (throw2 == 2) {
                return 1;
            }
            return 0;
        }
        return 0;
    }

    /**
     * Creates the status message that is returned to the
     * client at the end of each round.
     * @param winner The winner, either 1 for p1 or 2 for p2
     * @return A string containing some informative message for the players.
     */
    private String getStatusMessage(int winner) {
        if (roundWinner == 1) { //p1 wins
            wins1++;
            winStr = throwsArray[throw1] + " beats " + throwsArray[throw2];
        } else if (roundWinner == 2) { //p2 wins
            wins2++;
            winStr = throwsArray[throw2] + " beats " + throwsArray[throw1];
        } else { //tie
            winStr = "TIE! Throw again!";
        }
        return winStr;
    }

    /**
     * Creates the status message that is returned to the
     * client at the end of each round about the throws
     * @return A string containing throws of the round
     */
    private String getThrowMessage() {
        throwStr = p1 + " throws a " + throwsArray[throw1] + ". " + p2 + " throws a " + throwsArray[throw2] + ".";
        return throwStr;
    }

    public String getP1(){
        return new String(p1);
    }

    public String getP2(){
        return new String(p2);
    }

    public boolean getGameWon(){
        return gameWon;
    }
}
